Skip to content

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Jan 15, 2026

  • Updated ConnectionManager and MessageQueue to process PublishResult during acknowledgments (ACK/NACK).
  • Extended send_protocol_message to return PublishResult for publish tracking.
  • Bumped default protocol_version to 5.
  • Added tests for message update, delete, append operations, and PublishResult handling.

Summary by CodeRabbit

  • New Features

    • publish() now returns server acknowledgement details (including serials) and ACKs surface result objects.
    • Added message update, delete, and append operations plus retrieval of message versions.
    • Channels.get accepts additional channel option formats while preserving reattachment behavior.
  • Chores

    • Protocol version upgraded to support new realtime message capabilities.
  • Tests

    • Comprehensive tests added for publish/update/delete/append workflows across transports and encryption.

✏️ Tip: You can customize this high-level summary in your review settings.

@ttypic ttypic requested a review from owenpearson January 15, 2026 11:45
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Walkthrough

Adds realtime mutable-message support: protocol version bumped to 5, publish now returns server PublishResult, new update/delete/append message flows and REST helpers, ACK handling extended to carry PublishResult lists through ConnectionManager, and tests covering mutable-message behaviors.

Changes

Cohort / File(s) Summary
Protocol & Transport
ably/transport/defaults.py, ably/transport/websockettransport.py
Protocol version bumped from "2" to "5". WebSocket transport parses optional res from ACK ProtocolMessages into PublishResult.from_dict and forwards res to ConnectionManager.on_ack.
Connection Layer
ably/realtime/connectionmanager.py
PendingMessage.future typed as asyncio.Future[PublishResult]. PendingMessageQueue.complete_messages(...) accepts `res: list[PublishResult]
Realtime Channel API
ably/realtime/realtime_channel.py
publish() now returns PublishResult. Added _send_update() to emit update/delete/append ProtocolMessages and return UpdateDeleteResult. New helpers: update_message(), delete_message(), append_message(). Added get_message() and get_message_versions() REST delegations. Channels.get() accepts extra channel option kwargs.
Tests
test/ably/realtime/realtimechannelmutablemessages_test.py, test/ably/realtime/realtimechannel_publish_test.py
New pytest suite validating publish/update/delete/append flows (including encryption and multi-message publish) across JSON/MsgPack transports; updated publish test call-site to pass res to on_ack.

Sequence Diagram(s)

sequenceDiagram
    participant App as App
    participant Channel as RealtimeChannel
    participant Conn as ConnectionManager
    participant WS as WebSocketTransport
    participant Server as Server

    App->>Channel: publish()/update_message()/delete_message()/append_message()
    Channel->>Conn: send_protocol_message(pm, await_ack=True)
    Conn->>WS: emit WebSocket frame
    WS->>Server: send ProtocolMessage
    Server->>WS: ACK (serial, count, optional res[])
    WS->>Conn: on_ack(serial, count, res[])
    Conn->>Conn: complete_messages(serial, count, res)
    Conn->>Channel: resolve pending futures with PublishResult(s)
    Channel->>App: return PublishResult / UpdateDeleteResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped a protocol up to five,

ACKs now carry results alive.
I publish, update, append, delete with glee,
Futures resolve, versions hop with me.
Thump — mutable messages set free.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Realtime support for mutable messages, which aligns with the core changes across ConnectionManager, RealtimeChannel, and new test coverage.
Linked Issues check ✅ Passed The PR addresses AIT-258 requirements for Realtime mutable message support by implementing message update/delete/append operations, PublishResult handling in ACK/NACK flows, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are in-scope: ConnectionManager/WebSocketTransport ACK handling, RealtimeChannel mutable message operations, protocol version bump to support new features, and corresponding tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AIT-258/realtime-edits-deletes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/660/features January 15, 2026 11:46 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/ably/realtime/realtimechannelmutablemessages_test.py`:
- Around line 182-191: The test calls channel.publish with a keyword
(channel.publish(messages=messages)) but the publish implementation inspects
only positional args (checks len(args)), so kwargs are ignored and cause a
ValueError; change the call to pass the messages list positionally
(channel.publish(messages)) so publish receives the messages via args and the
test will exercise publish correctly.
🧹 Nitpick comments (2)
ably/realtime/connectionmanager.py (1)

99-109: Consider handling mismatched lengths between completed messages and results.

Using zip_longest ensures all completed messages are processed even when res_list is shorter or empty. However, if res_list is longer than completed_messages, msg will be None for the extra items, which could cause an AttributeError when accessing msg.future.

♻️ Suggested fix to guard against None msg
                 # Default res to empty list if None
                 res_list = res if res is not None else []
                 for (msg, publish_result) in zip_longest(completed_messages, res_list):
+                    if msg is None:
+                        continue
                     if msg.future and not msg.future.done():
                         if err:
                             msg.future.set_exception(err)
ably/realtime/realtime_channel.py (1)

964-981: Potential ambiguity when both options and kwargs are provided.

The current logic converts kwargs to ChannelOptions only when options is None. If a user accidentally provides both options and kwargs, the kwargs will be silently ignored. Consider raising an error or warning in this case.

♻️ Suggested fix to warn about ignored kwargs
         # Convert kwargs to ChannelOptions if provided
         if kwargs and not options:
             options = ChannelOptions(**kwargs)
+        elif kwargs and options:
+            log.warning("Channels.get(): kwargs ignored when options is provided")
         elif options and isinstance(options, dict):
             options = ChannelOptions.from_dict(options)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff2552 and 7c26134.

📒 Files selected for processing (5)
  • ably/realtime/connectionmanager.py
  • ably/realtime/realtime_channel.py
  • ably/transport/defaults.py
  • ably/transport/websockettransport.py
  • test/ably/realtime/realtimechannelmutablemessages_test.py
🧰 Additional context used
🧬 Code graph analysis (2)
ably/transport/websockettransport.py (2)
ably/realtime/connectionmanager.py (3)
  • ably (780-781)
  • on_ack (468-479)
  • count (59-61)
ably/types/operations.py (1)
  • PublishResult (48-67)
ably/realtime/connectionmanager.py (2)
ably/types/operations.py (1)
  • PublishResult (48-67)
ably/util/exceptions.py (1)
  • AblyException (9-90)
🪛 Gitleaks (8.30.0)
test/ably/realtime/realtimechannelmutablemessages_test.py

[high] 158-158: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: check (3.14)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.9)
🔇 Additional comments (19)
ably/transport/websockettransport.py (2)

15-15: LGTM!

Import of PublishResult is correctly added to support the new ACK handling flow.


176-179: LGTM!

The ACK handling correctly extracts the optional res field, converts each item to a PublishResult object when present, and passes it to the connection manager. The null-safety check before conversion is appropriate.

ably/realtime/connectionmanager.py (7)

7-7: LGTM!

Import of zip_longest is appropriate for aligning completed messages with their corresponding PublishResult entries.


34-34: LGTM!

Updated type annotation correctly reflects that the future now resolves to a PublishResult instead of None.


63-78: LGTM!

The updated signature correctly accepts the res parameter and the docstring accurately describes the new behavior.


216-253: LGTM!

The send_protocol_message method correctly returns the PublishResult when awaiting ACK-required messages and None otherwise. The flow handles both queued and immediate sends appropriately.


255-273: LGTM!

The _send_protocol_message_on_connected_state method correctly propagates the PublishResult from the future when ACK is required.


468-479: LGTM!

The on_ack method correctly accepts and forwards the res parameter to complete_messages.


493-493: LGTM!

Correctly passes None for res and the error to complete_messages for NACK handling.

ably/realtime/realtime_channel.py (5)

13-15: LGTM!

Imports are correctly added to support the new mutable message operations.


394-494: LGTM!

The publish method correctly returns the PublishResult from the connection manager, enabling callers to access message serials for subsequent update/delete operations.


526-606: Consider using MessageAction enum for action.name in log statement.

The _send_update implementation correctly validates serial presence, constructs the message with version metadata, handles encryption, and returns the UpdateDeleteResult. The logic is sound.

One minor observation: Line 588 uses action.name in the log, which assumes MessageAction is an enum with a name attribute - this should work correctly with Python enums.


608-672: LGTM!

The public convenience methods update_message, delete_message, and append_message are well-documented and correctly delegate to _send_update with the appropriate MessageAction values.


674-718: LGTM!

The REST delegation methods correctly call the parent Channel methods, providing a unified API for accessing messages via the REST API from the realtime channel.

test/ably/realtime/realtimechannelmutablemessages_test.py (4)

15-23: LGTM!

Good use of parametrization to test both JSON and MsgPack transports. The fixture properly configures the Ably client based on the transport type.


154-176: Test key is appropriate for testing - not a real secret.

The static analysis tool flagged 'keyfordecrypt_16' as a potential API key, but this is a test encryption key used only in unit tests and is not a real secret. This is a false positive.


266-278: LGTM!

The helper method wait_until_message_with_action_appears correctly uses polling to wait for a message with a specific action, with proper exception handling.


280-289: LGTM!

The helper method wait_until_get_all_message_version correctly polls for version history until the expected count is reached.

ably/transport/defaults.py (1)

2-2: Protocol version 5 is properly supported and already tested.

Ably Protocol v5 is officially supported with comprehensive test coverage for mutable messages (updates, deletes, appends) in both REST and realtime channels. The integration in connectionmanager.py correctly passes the protocol version, and the test infrastructure includes the required mutable messages support.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@ttypic ttypic force-pushed the AIT-196/rest-edits-deletes branch from 8ff2552 to 393693c Compare January 16, 2026 16:58
@ttypic ttypic force-pushed the AIT-258/realtime-edits-deletes branch from 7c26134 to d304d0a Compare January 19, 2026 10:40
@github-actions github-actions bot temporarily deployed to staging/pull/660/features January 19, 2026 10:41 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/ably/realtime/realtimechannel_publish_test.py (1)

456-458: Assertion may fail after the PublishResult changes.

With the updated complete_messages implementation, when res is None, the future is resolved with an empty PublishResult() object, not None. This assertion should be updated.

🔧 Suggested fix
         # Future should be resolved
         result = await asyncio.wait_for(publish_future, timeout=1)
-        assert result is None
+        assert result is not None
+        assert result.serials == []
♻️ Duplicate comments (1)
test/ably/realtime/realtimechannelmutablemessages_test.py (1)

182-191: The channel.publish(messages=messages) call is incorrect.

The publish method only processes positional arguments via len(args). Calling with messages=messages as a keyword argument will result in an empty args tuple, causing a ValueError.

🔧 Suggested fix
-        result = await channel.publish(messages=messages)
+        result = await channel.publish(messages)

@ttypic ttypic force-pushed the AIT-196/rest-edits-deletes branch 2 times, most recently from c921aa0 to 1723f5d Compare January 20, 2026 12:10
@ttypic ttypic force-pushed the AIT-258/realtime-edits-deletes branch from d304d0a to e843d08 Compare January 20, 2026 15:02
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@ably/realtime/connectionmanager.py`:
- Around line 99-109: The loop using zip_longest between completed_messages and
res_list can yield msg==None when res_list is longer, which causes accesses like
msg.future to raise; update the loop in connectionmanager.py (where zip_longest
is used) to either iterate directly over completed_messages and index into
res_list (e.g., for i, msg in enumerate(completed_messages): publish_result =
res_list[i] if i < len(res_list) else None) or add an explicit guard (if msg is
None: continue) before touching msg.future, and keep the existing logic that
substitutes PublishResult() when publish_result is None.

In `@ably/realtime/realtime_channel.py`:
- Around line 597-603: The code builds a protocol_message using params.items()
but params can be None; update the method in realtime_channel (the block
constructing protocol_message with ProtocolMessageAction.MESSAGE, self.name and
messages [msg_dict]) to guard against None by treating params as an empty dict
when not passed (e.g., use params = params or {} or a conditional variable)
before the dict comprehension, and then apply the existing {k: str(v).lower() if
type(v) is bool else v for k, v in params.items()} logic so update/delete/append
calls that omit params do not raise.
♻️ Duplicate comments (1)
test/ably/realtime/realtimechannelmutablemessages_test.py (1)

182-189: publish(messages=messages) will raise; pass list positionally instead.

publish() only inspects *args, so kwargs are ignored and cause a ValueError.

🛠️ Suggested fix
-        result = await channel.publish(messages=messages)
+        result = await channel.publish(messages)
🧹 Nitpick comments (1)
ably/realtime/connectionmanager.py (1)

216-223: Update the docstring return type.

send_protocol_message now returns PublishResult | None, but the docstring still states None.

@ttypic ttypic force-pushed the AIT-258/realtime-edits-deletes branch from e843d08 to f548267 Compare January 20, 2026 15:10
@github-actions github-actions bot temporarily deployed to staging/pull/660/features January 20, 2026 15:11 Inactive
@ttypic ttypic force-pushed the AIT-258/realtime-edits-deletes branch from f548267 to 995935b Compare January 20, 2026 15:12
@github-actions github-actions bot temporarily deployed to staging/pull/660/features January 20, 2026 15:13 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ably/realtime/connectionmanager.py (1)

216-223: Docstring return type is inconsistent with signature.

The docstring states Returns: None but the function signature and implementation return PublishResult | None.

📝 Suggested fix
         """Send a protocol message and optionally track it for acknowledgment

         Args:
             protocol_message: protocol message dict (new message)
         Returns:
-            None
+            PublishResult if the message requires acknowledgment, None otherwise
         """
♻️ Duplicate comments (1)
ably/realtime/connectionmanager.py (1)

99-109: zip_longest can still produce msg=None when res_list is longer.

The previous review flagged this issue. While the server should not send more results than messages, defensive coding would prevent a potential AttributeError if it does. The suggested fix was to iterate over completed_messages and index into res_list:

🛠️ Suggested defensive fix
-                res_list = res if res is not None else []
-                for (msg, publish_result) in zip_longest(completed_messages, res_list):
+                res_list = res or []
+                for idx, msg in enumerate(completed_messages):
+                    publish_result = res_list[idx] if idx < len(res_list) else None
                     if msg.future and not msg.future.done():
🧹 Nitpick comments (2)
test/ably/realtime/realtimechannelmutablemessages_test.py (2)

2-2: Consider using built-in list instead of typing.List.

Python 3.9+ supports using list directly in type annotations. The List import from typing is unnecessary if the project targets Python 3.9+.

📝 Suggested fix
 import logging
-from typing import List
 
 import pytest

And at line 281:

-        versions: List[Message] = []
+        versions: list[Message] = []

266-278: Consider catching specific exceptions in helper method.

The bare except Exception at line 273 catches all exceptions, which could mask unexpected errors during testing. Consider catching the specific exceptions expected (e.g., AblyException for not-found cases).

📝 Suggested fix
         async def check_message_action():
             nonlocal message
             try:
                 message = await channel.get_message(serial)
                 return message.action == action
-            except Exception:
+            except AblyException:
                 return False

- Updated `ConnectionManager` and `MessageQueue` to process `PublishResult` during acknowledgments (ACK/NACK).
- Extended `send_protocol_message` to return `PublishResult` for publish tracking.
- Bumped default `protocol_version` to 5.
- Added tests for message update, delete, append operations, and PublishResult handling.
Copy link
Member

@owenpearson owenpearson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 👍

Base automatically changed from AIT-196/rest-edits-deletes to main January 21, 2026 12:05
@ttypic ttypic merged commit 1ed0272 into main Jan 21, 2026
16 of 17 checks passed
@ttypic ttypic deleted the AIT-258/realtime-edits-deletes branch January 21, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants